/*
 * Copyright (C) 2017 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.google.android.filament;

import androidx.annotation.IntRange;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.Size;

/**
 * <code>IndirectLight</code> is used to simulate environment lighting, a form of global illumination.
 *
 * <p>Environment lighting has a two components:</p>
 * <ol>
 *  <li>irradiance</li>
 *  <li>reflections (specular component)</li>
 * </ol>
 *
 * <p>Environments are usually captured as high-resolution HDR equirectangular images and processed
 * by the <b>cmgen</b> tool to generate the data needed by {@link IndirectLight}.</p>
 *
 * <p>Currently {@link IndirectLight} is intended to be used for "distant probes", that is, to represent
 * global illumination from a distant (i.e. at infinity) environment, such as the sky or distant
 * mountains. Only a single {@link IndirectLight} can be used in a {@link Scene}.
 * This limitation will be lifted in the future.</p>
 *
 *
 * <h1>Creation and destruction</h1>
 *
 * <p>An {@link IndirectLight} object is created using the {@link IndirectLight.Builder} and
 * destroyed by calling {@link Engine#destroyIndirectLight}.</p>
 *
 * <pre>
 *  Engine engine = Engine.create();
 *
 *  Scene scene = engine.createScene();
 *
 *  IndirectLight environment = new IndirectLight.Builder()
 *              .reflections(cubemap)
 *              .irradiance(numBands, sphericalHarmonicsCoefficients)
 *              .build(engine);
 *
 *  scene.setIndirectLight(environment);
 * </pre>
 *
 *
 * <h1>Irradiance</h1>
 *
 * <p>The irradiance represents the light that comes from the environment and shines an
 * object's surface.
 * The irradiance is calculated automatically from the Reflections (see below), and generally
 * doesn't need to be provided explicitly.  However, it can be provided separately from the
 * Reflections as
 * <a href="https://en.wikipedia.org/wiki/Spherical_harmonics">Spherical Harmonics</a> (SH) of 1, 2 or
 * 3 bands, respectively 1, 4 or 9 coefficients.</p>
 *
 * <p>Use the <b>cmgen</b> tool to generate the Spherical Harmonics for a given environment.</p>
 *
 *
 * <h1>Reflections</h1>
 *
 * <p>The reflections on object surfaces (specular component) is calculated from a specially
 * filtered cubemap pyramid generated by the <b>cmgen</b> tool.</p>
 *
 * @see Scene
 * @see LightManager
 * @see Texture
 * @see Skybox
 */
public class IndirectLight {
    long mNativeObject;

    public IndirectLight(long indirectLight) {
        mNativeObject = indirectLight;
    }

    /**
     * Use <code>Builder</code> to construct an <code>IndirectLight</code> object instance.
     */
    public static class Builder {
        @SuppressWarnings({"FieldCanBeLocal", "UnusedDeclaration"}) // Keep to finalize native resources
        private final BuilderFinalizer mFinalizer;
        private final long mNativeBuilder;

        /**
         * Use <code>Builder</code> to construct an <code>IndirectLight</code> object instance.
         */
        public Builder() {
            mNativeBuilder = nCreateBuilder();
            mFinalizer = new BuilderFinalizer(mNativeBuilder);
        }

        /**
         * Set the reflections cubemap mipmap chain.
         *
         * @param cubemap   A mip-mapped cubemap generated by <b>cmgen</b>. Each cubemap level
         *                  encodes the irradiance for a roughness level.
         *
         * @return This Builder, for chaining calls.
         *
         */
        @NonNull
        public Builder reflections(@NonNull Texture cubemap) {
            nBuilderReflections(mNativeBuilder, cubemap.getNativeObject());
            return this;
        }

        /**
         * Sets the irradiance as Spherical Harmonics.
         *
         * <p>The irradiance coefficients must be pre-convolved by <code>&lt n &sdot l &gt</code> and
         * pre-multiplied by the Lambertian diffuse BRDF <code>1/&pi</code> and
         * specified as Spherical Harmonics coefficients.</p>
         *
         * <p>Additionally, these Spherical Harmonics coefficients must be pre-scaled by the
         * reconstruction factors A<sup>l,m</sup>.</p>
         *
         * <p>The final coefficients can be generated using the <code>cmgen</code> tool.</p>
         *
         * <p>The index in the <code>sh</code> array is given by:
         *  <br><code>index(l, m) = 3 &times (l * (l + 1) + m)</code>
         *  <br><code>sh[index(l,m) + 0] = L<sub>R</sub><sup>l,m</sup>
         *                      &times 1/&pi
         *                      &times A<sup>l,m</sup>
         *                      &times C<sup>l</sup> </code>
         *  <br><code>sh[index(l,m) + 1] = L<sub>G</sub><sup>l,m</sup>
         *                      &times 1/&pi
         *                      &times A<sup>l,m</sup>
         *                      &times C<sup>l</sup> </code>
         *  <br><code>sh[index(l,m) + 2] = L<sub>B</sub><sup>l,m</sup>
         *                      &times 1/&pi
         *                      &times A<sup>l,m</sup>
         *                      &times C<sup>l</sup> </code>
         * </p>
         *
         * <center>
         * <table border="1" cellpadding="3">
         *     <tr><th> index </th><th> l </th><th> m </th><th> A<sup>l,m</sup> </th><th> C<sup>l</sup> </th>
         *                  <th> 1/&pi &times A<sup>l,m</sup> &times C<sup>l</sup></th></tr>
         *     <tr align="right"><td>0</td><td>0</td><td> 0</td><td> 0.282095</td><td>3.1415926</td><td> 0.282095</td></tr>
         *     <tr align="right"><td>1</td><td>1</td><td>-1</td><td>-0.488602</td><td>2.0943951</td><td>-0.325735</td></tr>
         *     <tr align="right"><td>2</td><td>1</td><td> 0</td><td> 0.488602</td><td>2.0943951</td><td> 0.325735</td></tr>
         *     <tr align="right"><td>3</td><td>1</td><td> 1</td><td>-0.488602</td><td>2.0943951</td><td>-0.325735</td></tr>
         *     <tr align="right"><td>4</td><td>2</td><td>-2</td><td> 1.092548</td><td>0.785398 </td><td> 0.273137</td></tr>
         *     <tr align="right"><td>5</td><td>2</td><td>-1</td><td>-1.092548</td><td>0.785398 </td><td>-0.273137</td></tr>
         *     <tr align="right"><td>6</td><td>2</td><td> 0</td><td> 0.315392</td><td>0.785398 </td><td> 0.078848</td></tr>
         *     <tr align="right"><td>7</td><td>2</td><td> 1</td><td>-1.092548</td><td>0.785398 </td><td>-0.273137</td></tr>
         *     <tr align="right"><td>8</td><td>2</td><td> 2</td><td> 0.546274</td><td>0.785398 </td><td> 0.136569</td></tr>
         * </table>
         * </center>
         *
         *
         * <p>Only 1, 2 or 3 bands are allowed.</p>
         *
         * <p>Because the coefficients are pre-scaled, <code>sh[0]</code> is the environment's
         * average irradiance.</p>
         *
         * @param bands     Number of spherical harmonics bands. Must be 1, 2 or 3.
         * @param sh        Array containing the spherical harmonics coefficients.
         *                  The size of the array must be <code>3 &times bands<sup>2</sup></code>
         *                  (i.e. 1, 4 or 9 <code>float3</code> coefficients respectively).
         *
         * @return This Builder, for chaining calls.
         *
         * @exception ArrayIndexOutOfBoundsException if the <code>sh</code> array length is smaller
         *            than 3 &times bands<sup>2</sup>
         */
        @NonNull
        public Builder irradiance(@IntRange(from=1, to=3) int bands, @NonNull float[] sh) {
            switch (bands) {
                case 1: if (sh.length < 3)
                        throw new ArrayIndexOutOfBoundsException(
                            "1 band SH, array must be at least 1 x float3"); else break;
                case 2: if (sh.length < 4 * 3)
                        throw new ArrayIndexOutOfBoundsException(
                            "2 bands SH, array must be at least 4 x float3"); else break;
                case 3: if (sh.length < 9 * 3)
                        throw new ArrayIndexOutOfBoundsException(
                            "3 bands SH, array must be at least 9 x float3"); else break;
                default: throw new IllegalArgumentException("bands must be 1, 2 or 3");
            }
            nIrradiance(mNativeBuilder, bands, sh);
            return this;
        }

        /**
         * Sets the irradiance from the radiance expressed as Spherical Harmonics.
         *
         * <p>The radiance must be specified as Spherical Harmonics coefficients L<sup>l,m</sup>, where
         * each coefficient is comprised of three floats for red, green and blue components, respectively</p>
         *
         * <p>The index in the <code>sh</code> array is given by:
         *  <br><code>index(l, m) = 3 &times (l * (l + 1) + m)</code>
         *  <br><code>sh[index(l,m) + 0] = L<sub>R</sub><sup>l,m</sup></code>
         *  <br><code>sh[index(l,m) + 1] = L<sub>G</sub><sup>l,m</sup></code>
         *  <br><code>sh[index(l,m) + 2] = L<sub>B</sub><sup>l,m</sup></code>
         * </p>
         *
         * <center>
         * <table border="1" cellpadding="3">
         *     <tr><th> index </th><th> l </th><th> m </th>
         *     <tr align="right"><td>0</td><td>0</td><td> 0</td>
         *     <tr align="right"><td>1</td><td>1</td><td>-1</td>
         *     <tr align="right"><td>2</td><td>1</td><td> 0</td>
         *     <tr align="right"><td>3</td><td>1</td><td> 1</td>
         *     <tr align="right"><td>4</td><td>2</td><td>-2</td>
         *     <tr align="right"><td>5</td><td>2</td><td>-1</td>
         *     <tr align="right"><td>6</td><td>2</td><td> 0</td>
         *     <tr align="right"><td>7</td><td>2</td><td> 1</td>
         *     <tr align="right"><td>8</td><td>2</td><td> 2</td>
         * </table>
         * </center>
         *
         * @param bands     Number of spherical harmonics bands. Must be 1, 2 or 3.
         * @param sh        Array containing the spherical harmonics coefficients.
         *                  The size of the array must be 3 &times <code>bands<sup>2</sup></code>
         *                  (i.e. 1, 4 or 9 <code>float3</code> coefficients respectively).
         *
         * @return This Builder, for chaining calls.
         *
         * @exception ArrayIndexOutOfBoundsException if the <code>sh</code> array length is smaller
         *            than 3 &times bands<sup>2</sup>
         */
        @NonNull
        public Builder radiance(@IntRange(from=1, to=3) int bands, @NonNull float[] sh) {
            switch (bands) {
                case 1: if (sh.length < 3)
                        throw new ArrayIndexOutOfBoundsException(
                            "1 band SH, array must be at least 1 x float3"); else break;
                case 2: if (sh.length < 4 * 3)
                        throw new ArrayIndexOutOfBoundsException(
                            "2 bands SH, array must be at least 4 x float3"); else break;
                case 3: if (sh.length < 9 * 3)
                        throw new ArrayIndexOutOfBoundsException(
                            "3 bands SH, array must be at least 9 x float3"); else break;
                default: throw new IllegalArgumentException("bands must be 1, 2 or 3");
            }
            nRadiance(mNativeBuilder, bands, sh);
            return this;
        }

        /**
         * Sets the irradiance as a cubemap.
         * <p></p>
         * The irradiance can alternatively be specified as a cubemap instead of Spherical
         * Harmonics coefficients. It may or may not be more efficient, depending on your
         * hardware (essentially, it's trading ALU for bandwidth).
         * <p></p>
         * This irradiance cubemap can be generated with the <code>cmgen</code> tool.
         *
         * @param cubemap   Cubemap representing the Irradiance pre-convolved by
         *                  <code>&lt n &sdot l &gt</code>.
         *
         * @return This Builder, for chaining calls.
         *
         * @see #irradiance(int bands, float[] sh)
         */
        @NonNull
        public Builder irradiance(@NonNull Texture cubemap) {
            nIrradianceAsTexture(mNativeBuilder, cubemap.getNativeObject());
            return this;
        }

        /**
         * Environment intensity (optional).
         *
         * <p>Because the environment is encoded usually relative to some reference, the
         * range can be adjusted with this method.</p>
         *
         * @param envIntensity  Scale factor applied to the environment and irradiance such that
         *                      the result is in <i>lux</i>, or <i>lumen/m^2</i> (default = 30000)
         *
         * @return This Builder, for chaining calls.
         */
        @NonNull
        public Builder intensity(float envIntensity) {
            nIntensity(mNativeBuilder, envIntensity);
            return this;
        }

        /**
         * Specifies the rigid-body transformation to apply to the IBL.
         *
         * @param rotation 3x3 rotation matrix. Must be a rigid-body transform.
         *
         * @return This Builder, for chaining calls.
         */
        @NonNull
        public Builder rotation(@NonNull @Size(min = 9) float[] rotation) {
            nRotation(mNativeBuilder,
                    rotation[0], rotation[1], rotation[2],
                    rotation[3], rotation[4], rotation[5],
                    rotation[6], rotation[7], rotation[8]);
            return this;
        }

        /**
         * Creates the IndirectLight object and returns a pointer to it.
         *
         * @param engine The {@link Engine} to associate this <code>IndirectLight</code> with.
         *
         * @return A newly created <code>IndirectLight</code>
         *
         * @exception IllegalStateException if a parameter to a builder function was invalid.
         */
        @NonNull
        public IndirectLight build(@NonNull Engine engine) {
            long nativeIndirectLight = nBuilderBuild(mNativeBuilder, engine.getNativeObject());
            if (nativeIndirectLight == 0) throw new IllegalStateException("Couldn't create IndirectLight");
            return new IndirectLight(nativeIndirectLight);
        }

        private static class BuilderFinalizer {
            private final long mNativeObject;

            BuilderFinalizer(long nativeObject) { mNativeObject = nativeObject; }

            @Override
            public void finalize() {
                try {
                    super.finalize();
                } catch (Throwable t) { // Ignore
                } finally {
                    nDestroyBuilder(mNativeObject);
                }
            }
        }
    }

    /**
     * Sets the environment's intensity.
     *
     * <p>Because the environment is encoded usually relative to some reference, the
     * range can be adjusted with this method.</p>
     *
     * @param intensity  Scale factor applied to the environment and irradiance such that
     *                   the result is in <i>lux</i>, or <i>lumen/m^2</i> (default = 30000)
     */
    public void setIntensity(float intensity) {
        nSetIntensity(getNativeObject(), intensity);
    }

    /**
     * Returns the environment's intensity in <i>lux</i>, or <i>lumen/m^2</i>.
     */
    public float getIntensity() {
        return nGetIntensity(getNativeObject());
    }

    /**
     * Sets the rigid-body transformation to apply to the IBL.
     *
     * @param rotation 3x3 rotation matrix. Must be a rigid-body transform.
     */
    public void setRotation(@NonNull @Size(min = 9) float[] rotation) {
        Asserts.assertMat3fIn(rotation);
        nSetRotation(getNativeObject(),
                rotation[0], rotation[1], rotation[2],
                rotation[3], rotation[4], rotation[5],
                rotation[6], rotation[7], rotation[8]);
    }

    /**
     * Returns the rigid-body transformation applied to the IBL.
     *
     * @param rotation an array of 9 floats to receive the rigid-body transformation applied to
     *                 the IBL or <code>null</code>
     * @return the <code>rotation</code> paramter if it was provided, or a newly allocated float
     * array containing the rigid-body transformation applied to the IBL
     */
    @NonNull @Size(min = 9)
    public float[] getRotation(@Nullable @Size(min = 9) float[] rotation) {
        rotation = Asserts.assertMat3f(rotation);
        nGetRotation(getNativeObject(), rotation);
        return rotation;
    }

    /**
     * Helper to estimate the direction of the dominant light in the environment.
     *
     * <p>This assumes that there is only a single dominant light (such as the sun in outdoors
     * environments), if it's not the case the direction returned will be an average of the
     * various lights based on their intensity.</p>
     *
     * <p>If there are no clear dominant light, as is often the case with low dynamic range (LDR)
     * environments, this method may return a wrong or unexpected direction.</p>
     *
     * <p>The dominant light direction can be used to set a directional light's direction,
     * for instance to produce shadows that match the environment.</p>
     *
     * @param sh        pre-scaled 3-bands spherical harmonics
     * @param direction an array of 3 floats to receive a unit vector representing the direction of
     *                 the dominant light or <code>null</code>
     * @return the <code>direction</code> paramter if it was provided, or a newly allocated float
     * array containing a unit vector representing the direction of the dominant light
     *
     * @see LightManager.Builder#direction
     * @see #getColorEstimate
     */

    @NonNull @Size(min = 3)
    public static float[] getDirectionEstimate(@NonNull float[] sh, @Nullable @Size(min = 3) float[] direction) {
        if (sh.length < 9 * 3) {
            throw new ArrayIndexOutOfBoundsException(
                    "3 bands SH required, array must be at least 9 x float3");
        }
        direction = Asserts.assertFloat3(direction);
        nGetDirectionEstimateStatic(sh, direction);
        return direction;
    }

    /** @deprecated */
    @Deprecated
    @NonNull @Size(min = 3)
    public float[] getDirectionEstimate(@Nullable @Size(min = 3) float[] direction) {
        direction = Asserts.assertFloat3(direction);
        nGetDirectionEstimate(getNativeObject(), direction);
        return direction;
    }


    /**
     * Helper to estimate the color and relative intensity of the environment in a given direction.
     *
     * <p>This can be used to set the color and intensity of a directional light. In this case
     * make sure to multiply this relative intensity by the the intensity of this indirect light.</p>
     *
     * @param colorIntensity an array of 4 floats to receive the result or <code>null</code>
     * @param sh        pre-scaled 3-bands spherical harmonics
     * @param x the x coordinate of a unit vector representing the direction of the light
     * @param y the x coordinate of a unit vector representing the direction of the light
     * @param z the x coordinate of a unit vector representing the direction of the light
     *
     * @return A vector of 4 floats where the first 3 components represent the linear color and
     *         the 4th component represents the intensity of the dominant light
     *
     * @see LightManager.Builder#color
     * @see LightManager.Builder#intensity
     * @see #getDirectionEstimate
     * @see #getIntensity
     * @see #setIntensity
     */
    @NonNull @Size(min = 4)
    public static float[] getColorEstimate(@Nullable @Size(min = 4) float[] colorIntensity, @NonNull float[] sh, float x, float y, float z) {
        if (sh.length < 9 * 3) {
            throw new ArrayIndexOutOfBoundsException(
                    "3 bands SH required, array must be at least 9 x float3");
        }
        colorIntensity = Asserts.assertFloat4(colorIntensity);
        nGetColorEstimateStatic(colorIntensity, sh, x, y, z);
        return colorIntensity;
    }


    /** @deprecated */
    @Deprecated
    @NonNull @Size(min = 4)
    public float[] getColorEstimate(@Nullable @Size(min = 4) float[] colorIntensity, float x, float y, float z) {
        colorIntensity = Asserts.assertFloat4(colorIntensity);
        nGetColorEstimate(getNativeObject(), colorIntensity, x, y, z);
        return colorIntensity;
    }

    @Nullable
    public Texture getReflectionsTexture() {
        long nativeTexture = nGetReflectionsTexture(getNativeObject());
        return nativeTexture == 0 ? null : new Texture(nativeTexture);
    }

    @Nullable
    public Texture getIrradianceTexture() {
        long nativeTexture = nGetIrradianceTexture(getNativeObject());
        return nativeTexture == 0 ? null : new Texture(nativeTexture);
    }

    public long getNativeObject() {
        if (mNativeObject == 0) {
            throw new IllegalStateException("Calling method on destroyed IndirectLight");
        }
        return mNativeObject;
    }

    void clearNativeObject() {
        mNativeObject = 0;
    }

    private static native long nCreateBuilder();
    private static native void nDestroyBuilder(long nativeBuilder);

    private static native long nBuilderBuild(long nativeBuilder, long nativeEngine);
    private static native void nBuilderReflections(long nativeBuilder, long nativeTexture);
    private static native void nIrradiance(long nativeBuilder, int bands, float[] sh);
    private static native void nRadiance(long nativeBuilder, int bands, float[] sh);
    private static native void nIrradianceAsTexture(long nativeBuilder, long nativeTexture);
    private static native void nIntensity(long nativeBuilder, float envIntensity);
    private static native void nRotation(long nativeBuilder, float v0, float v1, float v2, float v3, float v4, float v5, float v6, float v7, float v8) ;

    private static native void nSetIntensity(long nativeIndirectLight, float intensity);
    private static native float nGetIntensity(long nativeIndirectLight);
    private static native void nSetRotation(long nativeIndirectLight, float v0, float v1, float v2, float v3, float v4, float v5, float v6, float v7, float v8);
    private static native void nGetRotation(long nativeIndirectLight, float[] outRotation);
    private static native void nGetDirectionEstimate(long nativeIndirectLight, float[] outDirection);
    private static native void nGetColorEstimate(long nativeIndirectLight, float[] outColor, float x, float y, float z);

    private static native long nGetReflectionsTexture(long nativeIndirectLight);
    private static native long nGetIrradianceTexture(long nativeIndirectLight);

    private static native void nGetDirectionEstimateStatic(float[] sh, float[] direction);
    private static native void nGetColorEstimateStatic(float[] colorIntensity, float[] sh, float x, float y, float z);
}
